type UIRef = any // UI library instance
interface $UUID {
    (): string
    value: string
    invoked: boolean
}
type MountedHandler = ($el: Element, $uuid: $UUID) => UIRef
type EffectHanlder =  (uiRef: UIRef) => void
type Updater = (...args: any) => void
type NamingUpdaters = {[name: string]: Updater}
type Effect = Updater | NamingUpdaters
type Props = {[name: string]: any}
// Component option is used to setup the component, but the connection with the component instance would be cut after initialize.
// Therefore we can not get the reactive data from componet option properties.
interface ComponentOption {
    [name: string]: any
    name?: string                        // The component name used to register globally
    template: string                     
    data?: () => {[name: string]: any}
    methods?: {[name: string]: Function}
    mounted?: () => void
    effect?: () => void
}


export function defineComponentFactory(handleMounted?: MountedHandler, handleEffect?: EffectHanlder) {
    return function defineComponent (factory: (props?: Props) => ComponentOption, componentName?: string) {
        return (defaultStore?: ProxyConstructor, defaultEffect?: Effect) => (props?: Props, store?: ProxyConstructor, effect?: Effect) => {
            let component = factory(props)
            const $uuid: $UUID = component.$uuid = function () {
                $uuid.invoked = true
                return $uuid.value 
            } as $UUID
            $uuid.value = `${componentName ? componentName : 'default'}-${(+new Date())}`
            component.$store = store || defaultStore || {}

            if (typeof component.data === 'function') {
                const kvs =  component.data()
                if (kvs) {
                    Object.defineProperties(component, Object.getOwnPropertyDescriptors(kvs))
                }
                else {
                    console.warn('`data` function return invalid value!')
                }
            }

            if (typeof component.methods === 'object') {
                for (let k in component.methods) {
                    component[k] = component.methods[k]
                }
            }

            const mountedFactory = (mounted?: (this: Record<string, any>) => void) => function(this: Record<string, any>, $child: Element) {
                component.$root = <Element>$child.parentElement
                if (handleMounted) {
                    component.$uiRef = handleMounted(component.$root, $uuid)
                }
                mounted && mounted.call(this)
            }
            component.$mounted = mountedFactory(component.mounted)


            effect = component.effect || effect || defaultEffect
            let effectIndex = 0
            const $effectFactory = (name: string, effect: Updater): Array<[string, string, string]> => {
                let $effectMethodName = `$effect${effectIndex++}`
                let paramNames = name.split(',').reduce((accu: string[], x: string) => {
                    if (x.trim() && x.trim() in component) {
                        accu.push(x.trim())
                    }
                    return accu
                }, [])
                let watchedEffects: Array<[string, string, string]> = []
                for (let paramName of paramNames) {
                    watchedEffects.push([paramName, paramNames.join(','), $effectMethodName])
                }

                component[$effectMethodName] = (...params: any) => {
                    handleEffect && (<Function>handleEffect)(component.$uiRef)
                    effect.apply(null, params)
                }

                return watchedEffects
            }

            let namingUpdaters: Array<[string, string, string]> = []
            if (effect) {
                if (effect instanceof Function) {
                    // track the v-model property only.
                    component.$effect = effect
                    namingUpdaters = [[null, null, '$effect']]
                }
                else {
                    // track all specific properties.
                    for (let watched in effect) {
                        namingUpdaters = namingUpdaters.concat($effectFactory(watched, effect[watched]))
                    }
                }
            }

            if (component.template) {
                component.$template = `<div data-id="fpv-$mounted" style="display:none" @mounted="$mounted($el)"></div>`
                    + namingUpdaters.map(([name, params, updaterName]) => `<div data-id="fpv-$effect" style="display:none" v-effect="$el.textContent=${name},${updaterName}(${params})"></div>`)
                                .reduce(($template, html) => {
                                    return html + $template
                                }, component.template)
            }

            return component
        }
    }
}

export const defineComponent = defineComponentFactory()